﻿using System;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using System.Xml.Xsl;
using CSW.Framework.Common;
using LinqBusinessObjectBuilder.Options;

namespace LinqBusinessObjectBuilder.Generator
{
    internal class SqlMetalModifier
    {
        /// <summary>
        /// Modifies SQLMetal output, generates classes, project files, and the solution file.
        /// </summary>
        /// <param name="solutionOutputDirectory">The solution output directory.</param>
        /// <param name="solutionName">Name of the solution.</param>
        /// <param name="inputFileName">The input file name.</param>
        /// <param name="outputFileName">The output file name.</param>
        /// <param name="options">The <see cref="GlobalOptions"/> object.</param>
        /// <returns>The path of the solution file.</returns>
        public static string Generate(string solutionOutputDirectory, string solutionName, string inputFileName, string outputFileName, GlobalOptions options)
        {
            Guard.ArgumentNotNullOrEmptyString(solutionOutputDirectory, "solutionOutputDirectory");
            Guard.ArgumentNotNullOrEmptyString(solutionName, "solutionName");
            Guard.ArgumentNotNullOrEmptyString(inputFileName, "inputFileName");
            Guard.ArgumentNotNullOrEmptyString(outputFileName, "outputFileName");
            Guard.ArgumentNotNull(options, "options");

            if (!File.Exists(options.CSWCommonPath))
                throw new FileNotFoundException(string.Format("'{0}' not found.", options.CSWCommonPath), options.CSWCommonPath);

            string[] lines = File.ReadAllLines(inputFileName);
            List<string> newLines = new List<string>();

            TableDictionary tableDictionary = new TableDictionary();
            string dataContextName = null;
            string currentTableName = null;
            string myNamespace = null;

            for (int index = 0; index < lines.Length; index++)
            {
                string line = lines[index];

                string trimmedLine = line.Trim();

                if (trimmedLine.StartsWith("namespace"))
                {
                    // Get the namespace

                    myNamespace = trimmedLine.Substring("namespace ".Length);
                }
                else if (trimmedLine == "public event PropertyChangedEventHandler PropertyChanged;")
                {
                    // Remove PropertyChangedEventHandler

                    // skip line
                    continue;
                }
                else if (trimmedLine == "protected virtual void SendPropertyChanged(String propertyName)")
                {
                    // Remove SendPropertyChanged method

                    // skip next 6 lines
                    index += 6;
                    continue;
                }
                else if (trimmedLine == "private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);")
                {
                    // Remove emptyChangingEventArgs definition, stored as static readonly in LinqBusinessObject

                    // skip following blank line
                    index += 2;

                    // change private fields to nullable
                    for (; index < lines.Length; index += 2)
                    {
                        line = lines[index];

                        if (line.Contains("#"))
                        {
                            index -= 2;
                            break;
                        }

                        int startIndex = line.IndexOf("private");
                        if (startIndex < 0)
                        {
                            index -= 2;
                            break;
                        }

                        startIndex = line.IndexOf(' ', startIndex);
                        if (startIndex < 0)
                        {
                            index -= 2;
                            break;
                        }

                        int endIndex = line.IndexOf(' ', startIndex + 1);
                        if (endIndex < 0)
                        {
                            index -= 2;
                            break;
                        }

                        string type = line.Substring(startIndex + 1, endIndex - startIndex - 1);
                        string newType = ConvertTypeToNullable(type);
                        if (type != newType)
                        {
                            line = line.Remove(startIndex + 1, endIndex - startIndex - 1);
                            line = line.Insert(startIndex + 1, newType);
                        }

                        newLines.Add(line);
                    }

                    continue;
                }
                else if (trimmedLine.StartsWith("public partial class") && trimmedLine.EndsWith(": System.Data.Linq.DataContext"))
                {
                    // Get the DataContext class' name

                    int startIndex = "public partial class".Length + 1;
                    int endIndex = trimmedLine.IndexOf(' ', startIndex);
                    dataContextName = trimmedLine.Substring(startIndex, endIndex - startIndex);

                    newLines.Add(line);
                    newLines.Add(lines[index + 1]);

                    // ConnectionString
                    newLines.Add("\t\tpublic static string ConnectionString");
                    newLines.Add("\t\t{");
                    newLines.Add("\t\t\tget");
                    newLines.Add("\t\t\t{");
                    newLines.Add("\t\t\t\tconst string name = \"" + dataContextName + "\";");
                    newLines.Add("\t\t\t\tSystem.Configuration.ConnectionStringSettings settings = System.Configuration.ConfigurationManager.ConnectionStrings[name];");
                    newLines.Add("\t\t\t\tif (settings == null)");
                    newLines.Add("\t\t\t\t\tthrow new Exception(string.Format(\"'{0}' connection string not found in configuration file.\", name));");
                    newLines.Add("\t\t\t\treturn settings.ConnectionString;");
                    newLines.Add("\t\t\t}");
                    newLines.Add("\t\t}");
                    newLines.Add("");

                    // Default constructor
                    newLines.Add(string.Format("\t\tpublic {0}() : this(ConnectionString)", dataContextName));
                    newLines.Add("\t\t{");
                    newLines.Add(string.Format("\t\t\tCSW.Framework.Common.Binding.LinqBusinessObject<{0}>.Add(this);", dataContextName));
                    newLines.Add("\t\t}");

                    // skip this line and next
                    index += 1;
                    continue;
                }
                else if (!string.IsNullOrEmpty(dataContextName) && trimmedLine.StartsWith("public " + dataContextName))
                {
                    // Make non-default constructors private

                    line = line.Replace("\t\tpublic ", "\t\tprivate ");
                }
                else if (trimmedLine.StartsWith("public partial class ") && trimmedLine.EndsWith(" : INotifyPropertyChanging, INotifyPropertyChanged"))
                {
                    // Add LinqBusinessObject as base class of Tables

                    line = line.Replace(" : INotifyPropertyChanging, INotifyPropertyChanged", string.Format(" : CSW.Framework.Common.Binding.LinqBusinessObject<{0}>, INotifyPropertyChanging, INotifyPropertyChanged", dataContextName));

                    int startIndex = "public partial class".Length + 1;
                    int endIndex = trimmedLine.IndexOf(' ', startIndex);
                    currentTableName = trimmedLine.Substring(startIndex, endIndex - startIndex);
                }
                //                else if (!string.IsNullOrEmpty(currentTableName) && trimmedLine.StartsWith("public " + currentTableName + "()"))
                //                {
                //                    line += " : base(" + dataContextName + ".Instance)";
                //                }
                else if (!string.IsNullOrEmpty(currentTableName) && trimmedLine.StartsWith("#region Extensibility Method Definitions"))
                {
                    // Get Partial Methods

                    newLines.Add(line);
                    for (index++; index < lines.Length; index++)
                    {
                        if (lines[index].Contains("#endregion"))
                        {
                            newLines.Add(lines[index]);
                            break;
                        }

                        line = ConvertMethodParameterToNullable(lines[index]);

                        tableDictionary[currentTableName].PartialMethods.Add(line);
                        newLines.Add(line);
                    }

                    // skip line
                    continue;
                }
                else if (trimmedLine.Contains("ThisKey"))
                {
                    // Get Foreign Key property names

                    int startIndex = trimmedLine.IndexOf("ThisKey") + "ThisKey".Length + 2;
                    int endIndex = trimmedLine.IndexOf('"', startIndex);
                    string meat = trimmedLine.Substring(startIndex, endIndex - startIndex);
                    string[] propertyNames = meat.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                    foreach (string propertyName in propertyNames)
                        tableDictionary[currentTableName].ForeignKeys.Add(propertyName);
                }
                else if (trimmedLine.StartsWith("[Column") &&
                         trimmedLine.Contains("AutoSync."))
                {
                    if (trimmedLine.Contains("AutoSync.Always") ||
                        trimmedLine.Contains("AutoSync.OnInsert") ||
                        trimmedLine.Contains("AutoSync.OnUpdate"))
                    {
                        // Change "set" to private for autosync properties

                        newLines.Add(line);
                        for (index++; index < lines.Length; index++)
                        {
                            line = lines[index];

                            if (line.Trim() == "set")
                            {
                                newLines.Add(line.Replace("set", "private set"));
                                break;
                            }

                            newLines.Add(line);
                        }

                        // skip line
                        continue;
                    }
                }

                newLines.Add(line);
            }

            // Pass 2
            currentTableName = null;
            for (int index = 0; index < newLines.Count; index++)
            {
                string line = newLines[index];
                if (line.StartsWith("    #") || line.StartsWith("    partial void"))
                {
                    // Fix indenting Partial Methods

                    newLines[index] = "\t\t" + line.Substring(4);
                }

                newLines[index] = CleanUpCodeLine(newLines[index]);

                string trimmedLine = line.Trim();

                if (currentTableName != null && trimmedLine.Contains("DbType=") && trimmedLine.Contains("Storage"))
                {
                    // Create PropertyInfo from property attributes

                    int dbTypeIndex = trimmedLine.IndexOf("DbType=");
                    int startIndex = trimmedLine.IndexOf('"', dbTypeIndex) + 1;
                    int endIndex = trimmedLine.IndexOf('"', startIndex) - 1;
                    string dbType = trimmedLine.Substring(startIndex, endIndex - startIndex + 1).ToUpper();

                    string nextLine = newLines[index + 1];

                    int publicIndex = nextLine.IndexOf("public ");
                    int typeStartIndex = publicIndex + "public ".Length;
                    int spaceIndex = nextLine.IndexOf(' ', typeStartIndex);

                    string name = nextLine.Substring(spaceIndex + 1);

                    int typeLength = spaceIndex - typeStartIndex;
                    string oldType = nextLine.Substring(typeStartIndex, typeLength);
                    string type = ConvertTypeToNullable(oldType);

                    if (type != oldType)
                    {
                        nextLine = nextLine.Remove(typeStartIndex, typeLength);
                        nextLine = nextLine.Insert(typeStartIndex, type);
                        newLines[index + 1] = nextLine;
                    }

                    PropertyInfo propertyInfo = new PropertyInfo
                                                {
                                                    PropertyName = name,
                                                    Type = type,
                                                    IsReadOnly = dbType.Contains("IDENTITY"),
                                                    IsRequired = dbType.Contains("NOT NULL"),
                                                    IsIdentity = dbType.Contains("IDENTITY")
                                                };

                    int charIndex = dbType.IndexOf("CHAR(");
                    if (charIndex >= 0)
                    {
                        int endParenIndex = dbType.IndexOf(')', charIndex);
                        string strLength = dbType.Substring(charIndex + 5, endParenIndex - charIndex - 5);
                        int length;
                        if (int.TryParse(strLength, out length))
                        {
                            propertyInfo.MaxLength = length;
                        }
                    }

                    bool found = false;
                    foreach (PropertyInfo pi in tableDictionary[currentTableName].Properties)
                    {
                        if (pi.PropertyName == propertyInfo.PropertyName)
                        {
                            found = true;
                            break;
                        }
                    }

                    if (!found)
                        tableDictionary[currentTableName].Properties.Add(propertyInfo);
                }

                if (trimmedLine.StartsWith("public partial class ") && trimmedLine.EndsWith(string.Format("LinqBusinessObject<{0}>, INotifyPropertyChanging, INotifyPropertyChanged", dataContextName)))
                {
                    // Get current table name

                    int startIndex = "public partial class".Length + 1;
                    int endIndex = trimmedLine.IndexOf(' ', startIndex);
                    currentTableName = trimmedLine.Substring(startIndex, endIndex - startIndex);
                }
                else if (currentTableName != null && trimmedLine.StartsWith("public"))
                {
                    // Set foreign key direct values to private set

                    foreach (string propertyName in tableDictionary[currentTableName].ForeignKeys)
                    {
                        if (trimmedLine.EndsWith(" " + propertyName))
                        {
                            for (; index < newLines.Count; index++)
                            {
                                newLines[index] = CleanUpCodeLine(newLines[index]);

                                trimmedLine = newLines[index].Trim();

                                if (trimmedLine == "set")
                                {
                                    newLines[index] = newLines[index].Replace("set", "private set");
                                    break;
                                }

                                if (trimmedLine.EndsWith(" set"))
                                    break;
                            }

                            break;
                        }
                    }
                }
                else if (trimmedLine.Contains("IsForeignKey=true"))
                {
                    // Store EntityRef's
                    // Store OnChanging in partial methods to be added to Table's partial method list
                    // Embed call to new OnChanging partial method in property setter

                    trimmedLine = newLines[index + 1].Trim();
                    int propertyStartIndex = trimmedLine.IndexOf(' ', "public ".Length) + 1;
                    string propertyName = trimmedLine.Substring(propertyStartIndex);
                    int typeStartIndex = trimmedLine.IndexOf(' ', "public".Length) + 1;
                    string type = trimmedLine.Substring(typeStartIndex, propertyStartIndex - typeStartIndex - 1);

                    string methodName = "On" + propertyName + "Changing";
                    string methodDeclaration = "partial void " + methodName + "(" + type + " value);";

                    tableDictionary[currentTableName].EntityRefs.Add(methodDeclaration);
                    tableDictionary[currentTableName].PartialMethods.Add(methodDeclaration);

                    for (; index < newLines.Count; index++)
                    {
                        const string defaultText = "default(";

                        newLines[index] = CleanUpCodeLine(newLines[index]);

                        trimmedLine = newLines[index].Trim();
                        if (trimmedLine == "SendPropertyChanging();")
                        {
                            newLines.Insert(index + 1, "\t\t\t\t\t" + methodName + "(value);");
                        }
                        else if (trimmedLine.Contains(defaultText))
                        {
                            int indexOfDefault = newLines[index].IndexOf(defaultText);
                            int indexOfCloseParen = newLines[index].IndexOf(')', indexOfDefault);
                            int meatStartIndex = indexOfDefault + defaultText.Length;
                            string meat = newLines[index].Substring(meatStartIndex, indexOfCloseParen - meatStartIndex);
                            if (!meat.StartsWith("Nullable") && !meat.EndsWith("?") && meat.ToLower() != "string")
                            {
                                newLines[index] = newLines[index].Insert(indexOfCloseParen, "?");
                            }
                        }
                        else if (trimmedLine.Contains("SendPropertyChanged"))
                        {
                            break;
                        }
                    }
                }
            }

            // Pass 3 - Add partial methods
            currentTableName = null;
            for (int index = 0; index < newLines.Count; index++)
            {
                string trimmedLine = newLines[index].Trim();

                if (trimmedLine.StartsWith("public partial class ") && trimmedLine.EndsWith(string.Format("LinqBusinessObject<{0}>, INotifyPropertyChanging, INotifyPropertyChanged", dataContextName)))
                {
                    // Get table name

                    int startIndex = "public partial class".Length + 1;
                    int endIndex = trimmedLine.IndexOf(' ', startIndex);
                    currentTableName = trimmedLine.Substring(startIndex, endIndex - startIndex);
                }
                else if (currentTableName != null && trimmedLine == "#region Extensibility Method Definitions")
                {
                    // Replace Partial Method list with stored and newly created partial method declarations.

                    for (; index < newLines.Count; index++)
                    {
                        if (newLines[index].Trim() == "#endregion")
                        {
                            foreach (string methodDeclaration in tableDictionary[currentTableName].EntityRefs)
                            {
                                newLines.Insert(index, "\t\t" + methodDeclaration);
                            }

                            break;
                        }
                    }
                }
                else if (currentTableName != null && trimmedLine.Contains("ThisKey"))
                {
                    // Replace explicit foreign key Property Info with EntityRef

                    int startIndex = trimmedLine.IndexOf("ThisKey") + "ThisKey".Length + 2;
                    int endIndex = trimmedLine.IndexOf('"', startIndex);
                    string[] propertyNames = trimmedLine.Substring(startIndex, endIndex - startIndex).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                    bool isReadOnly = false;
                    bool isRequired = false;
                    foreach (string propertyName in propertyNames)
                    {
                        foreach (PropertyInfo sets in tableDictionary[currentTableName].Properties)
                        {
                            if (sets.PropertyName == propertyName)
                            {
                                isReadOnly = isReadOnly || sets.IsReadOnly;
                                isRequired = isRequired || sets.IsRequired;
                                tableDictionary[currentTableName].Properties.Remove(sets);
                                break;
                            }
                        }
                    }

                    trimmedLine = newLines[index + 1].Trim();
                    int propertyStartIndex = trimmedLine.IndexOf(' ', "public ".Length) + 1;
                    string refPropertyName = trimmedLine.Substring(propertyStartIndex);
                    int spaceIndex = trimmedLine.IndexOf(' ');
                    string type = trimmedLine.Substring(spaceIndex + 1, propertyStartIndex - spaceIndex - 2);

                    bool found = false;
                    foreach (PropertyInfo sets in tableDictionary[currentTableName].Properties)
                    {
                        if (sets.PropertyName == refPropertyName)
                        {
                            found = true;
                            break;
                        }
                    }

                    if (!found)
                    {
                        PropertyInfo set = new PropertyInfo
                                               {
                                                   PropertyName = refPropertyName,
                                                   Type = type,
                                                   MaxLength = null,
                                                   IsReadOnly = isReadOnly,
                                                   IsRequired = isRequired,
                                                   IsEntityRef = true
                                               };
                        tableDictionary[currentTableName].Properties.Add(set);
                    }
                }
            }

            string outputDirectory = Path.GetDirectoryName(outputFileName);
            if (!Directory.Exists(outputDirectory))
                Directory.CreateDirectory(outputDirectory);

            // DataContext file
            File.WriteAllLines(outputFileName, newLines.ToArray());
            CleanUpCSharp(outputFileName);

            List<string> projectFiles = GeneratePartialClasses(dataContextName, outputDirectory, myNamespace, tableDictionary);

            string appPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            string projectFileInputPath = Path.Combine(appPath, @"Templates\Business\Business.csproj.xslt");
            string projectFileOutputPath = Path.Combine(outputDirectory, (myNamespace ?? dataContextName) + ".csproj");
            string solutionFileInputPath = Path.Combine(appPath, @"Templates\Solution.sln.xslt");
            string solutionFileOutputPath = Path.Combine(solutionOutputDirectory, solutionName);
            if (Path.GetExtension(solutionFileOutputPath).ToLower() != ".sln")
                solutionFileOutputPath += ".sln";

            string projectGuid = Guid.NewGuid().ToString().ToUpper();
            string projectAssemblyName = myNamespace ?? dataContextName;

            // fileNameStartIndex is used to get relative paths from the project root
            int fileNameStartIndex = outputDirectory.Length + (outputDirectory.EndsWith(@"\") ? 0 : 1);
            string shortOutputFileName = outputFileName.Substring(fileNameStartIndex);

            XElement xml = new XElement("project",
                             new[] {
                               new XElement("guid", projectGuid),
                               new XElement("root_namespace", myNamespace),
                               new XElement("assembly_name", projectAssemblyName),
                               new XElement("name", shortOutputFileName)
                             }
                           );

            xml.Add(new XElement("file", new XElement("filename", shortOutputFileName)));
            xml.Add(projectFiles.Select(p => new XElement("file", new XElement("filename", p.Substring(fileNameStartIndex)))));

            xml.Add(new XElement("project_reference", new[] { 
                                                        new XElement("path", options.CSWCommonPath),
                                                        new XElement("guid", "B946DC07-1745-49F4-9614-EA803E4763A5"),
                                                        new XElement("name", "CSW.Framework.Common")
                                                      }));

            // .Business.csproj
            if (!File.Exists(projectFileOutputPath))
            {
                XslCompiledTransform xslTran = new XslCompiledTransform();
                xslTran.Load(projectFileInputPath);
                using (StreamWriter writer = new StreamWriter(File.Open(projectFileOutputPath, FileMode.Create, FileAccess.Write, FileShare.None)))
                {
                    xslTran.Transform((new XDocument(xml)).CreateReader(), null, writer);
                }
            }
            else
            {
                XDocument xBusProj = XDocument.Load(projectFileOutputPath);
                XElement projectElement = xBusProj.Root;
                if (projectElement == null)
                    throw new InvalidDataException(string.Format("{0} does not contain a root element", projectFileOutputPath));
                IEnumerable<XElement> itemGroups = projectElement.Elements().Where(p => p.Name.LocalName == "ItemGroup");

                XElement compileItemGroup = itemGroups.Where(p => p.Elements().Where(q => q.Name.LocalName == "Compile").Any()).SingleOrDefault();
                if (compileItemGroup == null)
                    throw new InvalidDataException(string.Format("{0} does not contain a single ItemGroup containing Compile elements", projectFileOutputPath));

                IEnumerable<XElement> fileElements = xml.Elements("file");
                foreach (XElement compileItem in compileItemGroup.Elements().Where(p => p.Name.LocalName == "Compile"))
                {
                    string value = compileItem.Attributes().Where(p => p.Name.LocalName == "Include").Single().Value;

                    XElement element = fileElements.Where(p => p.Element("filename").Value == value).SingleOrDefault();
                    if (element != null)
                    {
                        element.Remove();
                    }
                }

                foreach (XElement element in fileElements)
                {
                    compileItemGroup.Add(
                        new XElement(
                            "{http://schemas.microsoft.com/developer/msbuild/2003}Compile",
                            new XAttribute(
                                "Include",
                                element.Element("filename").Value
                            )
                        )
                    );
                }

                if (fileElements.Any())
                    xBusProj.Save(projectFileOutputPath);
            }

            // Solution file
            // Do not overwrite if already exists
            if (!File.Exists(solutionFileOutputPath))
            {
                xml = new XElement("solution");

                xml.Add(new XElement("guid", Guid.NewGuid().ToString().ToUpper()));

                xml.Add(new XElement("project", new[] { 
                    new XElement("path", options.CSWCommonPath),
                    new XElement("guid", "B946DC07-1745-49F4-9614-EA803E4763A5"),
                    new XElement("assembly_name", "CSW.Framework.Common")
                }));

                xml.Add(new XElement("project", new[] { 
                    new XElement("path", options.CSWUIPath),
                    new XElement("guid", "C760B2CF-81CA-4750-8F8F-5E974590C8C9"),
                    new XElement("assembly_name", "CSW.Framework.UI")
                }));

                xml.Add(new XElement("project", new[] { 
                    new XElement("path", options.CSWUIDevExPath),
                    new XElement("guid", "A8411613-5E20-4ACD-9080-DE4A7CE7F6B8"),
                    new XElement("assembly_name", "CSW.Framework.UI.DevEx")
                }));

                xml.Add(new XElement("project", new[] { 
                    new XElement("path", Global.GetRelativePath(solutionOutputDirectory, projectFileOutputPath)),
                    new XElement("guid", projectGuid),
                    new XElement("assembly_name", projectAssemblyName)
                }));

                XslCompiledTransform xslTran = new XslCompiledTransform();
                xslTran.Load(solutionFileInputPath);
                using (StreamWriter writer = new StreamWriter(File.Open(solutionFileOutputPath, FileMode.Create, FileAccess.Write, FileShare.None)))
                {
                    xslTran.Transform((new XDocument(xml)).CreateReader(), null, writer);
                }
            }

            return solutionFileOutputPath;
        }

        private static string ConvertMethodParameterToNullable(string methodDeclaration)
        {
            Guard.ArgumentNotNullOrEmptyString(methodDeclaration, "methodDeclaration");

            int startIndex = methodDeclaration.IndexOf('(');
            int endIndex = methodDeclaration.IndexOf(')');

            if (startIndex < 0 || endIndex < startIndex)
                return methodDeclaration;

            int spaceIndex = methodDeclaration.IndexOf(' ', startIndex);
            if (spaceIndex < startIndex)
                return methodDeclaration;

            string parameterType = methodDeclaration.Substring(startIndex + 1, spaceIndex - startIndex - 1);

            string newParameterType = ConvertTypeToNullable(parameterType);
            if (parameterType != newParameterType)
            {
                methodDeclaration = methodDeclaration.Remove(startIndex + 1, spaceIndex - startIndex - 1);
                methodDeclaration = methodDeclaration.Insert(startIndex + 1, newParameterType);
            }

            return methodDeclaration;
        }

        private static string CleanUpCodeLine(string line)
        {
            if (line == null)
                return null;

            if (!line.Contains("using "))
            {
                line = line.Replace("System.Data.Linq.Mapping.", "");
                line = line.Replace("System.Data.Linq.", "");
                line = line.Replace("System.Data.", "");
                line = line.Replace(" this.", " ");
                line = line.Replace("\tthis.", "\t");
                line = line.Replace("(this.", "(");
            }

            return line;
        }

        private static List<string> GeneratePartialClasses(string dataContextName, string outputDirectory, string myNamespace, TableDictionary tableDictionary)
        {
            Guard.ArgumentNotNullOrEmptyString(dataContextName, "dataContextName");
            Guard.ArgumentNotNullOrEmptyString(outputDirectory, "outputDirectory");
            Guard.ArgumentNotNull(tableDictionary, "tableDictionary");

            List<string> projectFiles = new List<string>();

            foreach (KeyValuePair<string, TableInfo> kvp in tableDictionary)
            {
                projectFiles.Add(GenerateClass(dataContextName, outputDirectory, myNamespace, kvp.Value));
                projectFiles.Add(GenerateBackingClass(outputDirectory, myNamespace, kvp.Value));
            }

            return projectFiles;
        }

        private static string GenerateClass(string dataContextName, string outputDirectory, string myNamespace, TableInfo table)
        {
            Guard.ArgumentNotNullOrEmptyString(dataContextName, "dataContextName");
            Guard.ArgumentNotNullOrEmptyString(outputDirectory, "outputDirectory");
            Guard.ArgumentNotNull(table, "table");

            string fileName = Path.Combine(Path.Combine(outputDirectory, "Tables"), table.TableName) + ".cs";

            // Do not overwrite user editable file
            if (File.Exists(fileName))
                return fileName;

            List<string> output = new List<string>
            {
                                          "//------------------------------------------------------------------------------",
                                          "// <auto-generated>",
                                          "//     This code was generated by LINQ Business Object Builder.",
                                          "//     Version: " + Assembly.GetExecutingAssembly().GetName().Version,
                                          "//",
                                          "//     Changes to this file will be PRESERVED if the code is regenerated.",
                                          "//",
                                          "//     Omitting the OnCreated, OnLoaded methods or calls to BeginOnCreated,",
                                          "//     BeginOnLoaded may cause incorrect behavior.",
                                          "// </auto-generated>",
                                          "//------------------------------------------------------------------------------",
                                          "",
                                          "using System;",
                                          "using System.Linq;",
                                          "using System.Data.Linq;",
                                          "using System.ComponentModel;",
                                          "using CSW.Framework.Common;",
                                          "using CSW.Framework.Common.Binding;",
                                          ""
            };

            PropertyInfo identityColumn = null;
            string singleStringProperty = null;
            bool foundStringProperty = false;
            foreach (PropertyInfo pi in table.Properties)
            {
                if (pi.Type == "string")
                {
                    if (!foundStringProperty)
                    {
                        singleStringProperty = pi.PropertyName;
                        foundStringProperty = true;
                    }
                    else
                    {
                        singleStringProperty = null;
                    }
                }

                if (pi.IsIdentity)
                {
                    identityColumn = pi;
                }
            }

            if (!string.IsNullOrEmpty(myNamespace))
            {
                output.Add("namespace " + myNamespace);
                output.Add("{");
            }

            output.Add("\t/// <summary>");
            output.Add("\t/// " + table.TableName);
            output.Add("\t/// </summary>");
            output.Add("\tpartial class " + table.TableName);
            output.Add("\t{");

            output.Add("\t\tpartial void OnCreated()");
            output.Add("\t\t{");
            output.Add("\t\t\tBeginOnCreated(); // Keep this line above custom code");
            output.Add("");
            output.Add("\t\t\t// Called immediately after the constructor.");
            output.Add("");
            output.Add("\t\t\t#region Quick Tips");
            output.Add("\t\t\t// 1. Subscribe to m_PropertyName.PropertyChanged to respond to a changed value.");
            output.Add("\t\t\t// 2. Use AddCheckConstraint(m_PropertyName, private void CheckMethodName(ErrorMessage errorMessage));");
            output.Add("\t\t\t//    to add validation logic. If validation fails call errorMessage.Append(string)");
            output.Add("\t\t\t//    inside the check method.");
            output.Add("\t\t\t// 3. If validation depends on another property add one (or many) [DependsOn(\"\")]");
            output.Add("\t\t\t//    attribute(s) to the check method.");
            output.Add("\t\t\t// 4. Set m_PropertyName.IsReadOnly to toggle read only status from the");
            output.Add("\t\t\t//    user's perspective.");
            output.Add("\t\t\t// 5. To override a read only property use m_PropertyName.OverrideReadOnlyValue.");
            output.Add("\t\t\t// 6. Use AddRequiredField to add a check constraint for a required field.");
            output.Add("\t\t\t//    Once AddRequiredField is called, you can turn on/off the requirement by using");
            output.Add("\t\t\t//    m_Property.IsRequired; however, setting IsRequired without first using");
            output.Add("\t\t\t//    AddRequiredField has no effect. Fields that are NOT NULL in the database already");
            output.Add("\t\t\t//    use AddRequiredField in the TableName.Generated.cs file.");
            output.Add("\t\t\t#endregion");
            output.Add("");
            output.Add("\t\t}");
            output.Add("");
            output.Add("\t\tpartial void OnLoaded()");
            output.Add("\t\t{");
            output.Add("\t\t\tBeginOnLoaded(); // Keep this line above custom code");
            output.Add("");
            output.Add("\t\t\t// Occurs after an existing record has been loaded from the database.");
            output.Add("");
            output.Add("\t\t}");
            output.Add("");
            output.Add("\t\t//stub code for short lists");
            output.Add("\t\t//public static BindingList<" + table.TableName + "> GetAll(DataContext dataContext)");
            output.Add("\t\t//{");
            output.Add("\t\t//\tGuard.ArgumentNotNull(dataContext, \"dataContext\");");
            output.Add("\t\t//\tif (!(dataContext is " + dataContextName + "))");
            output.Add("\t\t//\t\tthrow new ArgumentException(\"dataContext is not of type \" + typeof(" + dataContextName + ").FullName, \"dataContext\");");
            output.Add("\t\t//");
            string orderBy = identityColumn == null ? null : ".OrderBy(p => p." + identityColumn.PropertyName + ")";
            output.Add(string.Format("\t\t//\treturn new BindingList<{0}>(dataContext.GetTable<{0}>(){1}.ToList());", table.TableName, orderBy));
            output.Add("\t\t//}");
            output.Add("");

            // ToString
            output.Add(string.Format("\t\t/// <summary>Returns a <see cref=\"String\"/> that represents the current <see cref=\"{0}\"/>.</summary>", table.TableName));
            output.Add(string.Format("\t\t/// <returns>A <see cref=\"String\"/> that represents the current <see cref=\"{0}\"/>.</returns>", table.TableName));
            output.Add("\t\tpublic override string ToString()");
            output.Add("\t\t{");
            if (!string.IsNullOrEmpty(singleStringProperty))
            {
                output.Add(string.Format("\t\t\treturn {0};", singleStringProperty));
            }
            else
            {
                if (identityColumn != null && !foundStringProperty)
                    output.Add(string.Format("\t\t\treturn {0}.HasValue ? \"{1} \" + {0} : \"<not saved>\";", identityColumn.PropertyName, table.TableName));
                else
                    output.Add(string.Format("\t\t\treturn \"'{0}': Override ToString() to display text\";", table.TableName));
            }
            output.Add("\t\t}");
            output.Add("");

            if (identityColumn != null)
            {
                output.Add("\t\t#region Equals and GetHashCode Definitions");
                output.Add("");

                // Equals
                output.Add(string.Format("\t\t/// <summary>Determines whether the specified <see cref=\"T:System.Object\"></see> is equal to the current <see cref=\"{0}\"></see>.</summary>", table.TableName));
                output.Add(string.Format("\t\t/// <param name=\"obj\">The <see cref=\"T:System.Object\"></see> to compare with the current <see cref=\"{0}\"></see>.</param>", table.TableName));
                output.Add(string.Format("\t\t/// <returns><c>true</c> if the specified <see cref=\"T:System.Object\"></see> is equal to the current <see cref=\"{0}\"></see>; otherwise, <c>false</c>.</returns>", table.TableName));
                output.Add("\t\tpublic override bool Equals(object obj)");
                output.Add("\t\t{");
                output.Add(string.Format("\t\t\t{0} item = obj as {0};", table.TableName));
                output.Add("\t\t\tif (item == null)");
                output.Add("\t\t\t\treturn false;");
                output.Add("");
                output.Add("\t\t\tif (item == this)");
                output.Add("\t\t\t\treturn true;");
                output.Add("");
                output.Add(string.Format("\t\t\tif ({0} == null)", identityColumn.PropertyName));
                output.Add("\t\t\t\treturn false;");
                output.Add("");
                output.Add(string.Format("\t\t\treturn ({0} == item.{0});", identityColumn.PropertyName));
                output.Add("\t\t}");
                output.Add("");

                // GetHashCode
                output.Add(string.Format("\t\t/// <summary>Gets the hash code for the current <see cref=\"{0}\"></see>.</summary>", table.TableName));
                output.Add(string.Format("\t\t/// <returns>A hash code for the current <see cref=\"{0}\"></see>.</returns>", table.TableName));
                output.Add("\t\tpublic override int GetHashCode()");
                output.Add("\t\t{");
                output.Add(string.Format("\t\t\tif (!{0}.HasValue)", identityColumn.PropertyName));
                output.Add("\t\t\t\treturn base.GetHashCode();");
                output.Add("");
                output.Add(string.Format("\t\t\treturn {0}.Value;", identityColumn.PropertyName));
                output.Add("\t\t}");
                output.Add("");

                output.Add("\t\t#endregion Equals and GetHashCode Definitions");
                output.Add("");
            }
            output.Add("\t}");

            if (!string.IsNullOrEmpty(myNamespace))
            {
                output.Add("}");
            }

            string myOutputDirectory = Path.GetDirectoryName(fileName);
            if (!Directory.Exists(myOutputDirectory))
                Directory.CreateDirectory(myOutputDirectory);

            // User editable file
            File.WriteAllLines(fileName, output.ToArray());
            CleanUpCSharp(fileName);

            return fileName;
        }

        private static string GenerateBackingClass(string outputDirectory, string myNamespace, TableInfo table)
        {
            Guard.ArgumentNotNullOrEmptyString(outputDirectory, "outputDirectory");
            Guard.ArgumentNotNull(table, "table");

            string fileName = Path.Combine(Path.Combine(outputDirectory, @"Tables\Generated"), table.TableName) + ".Generated.cs";

            List<string> output = new List<string>
                                      {
                                          "//------------------------------------------------------------------------------",
                                          "// <auto-generated>",
                                          "//     This code was generated by LINQ Business Object Builder.",
                                          "//     Version: " + Assembly.GetExecutingAssembly().GetName().Version,
                                          "//",
                                          "//     Changes to this file may cause incorrect behavior and will be lost if",
                                          "//     the code is regenerated.",
                                          "// </auto-generated>",
                                          "//------------------------------------------------------------------------------",
                                          "",
                                          "using System;",
                                          "using System.Data.Linq;",
                                          ""
                                      };
            if (!string.IsNullOrEmpty(myNamespace))
            {
                output.Add("namespace " + myNamespace);
                output.Add("{");
            }

            output.Add("\t/// <summary>");
            output.Add("\t/// " + table.TableName);
            output.Add("\t/// </summary>");
            output.Add("\tpartial class " + table.TableName);
            output.Add("\t{");

            foreach (PropertyInfo sets in table.Properties)
            {
                output.Add(string.Format("\t\tprivate Property<{0}> m_{1};", sets.Type, sets.PropertyName));
            }
            output.Add("");

            foreach (string partialMethod in table.PartialMethods)
            {
                string methodDeclaration = partialMethod.Trim().TrimEnd(';');

                string methodName = methodDeclaration.Replace("partial void ", "");
                int parenIndex = methodName.IndexOf("(");
                methodName = methodName.Substring(0, parenIndex);

                if (methodName.EndsWith("Changed") || methodName == "OnLoaded" || methodName == "OnValidate")
                    continue;

                //methodDeclaration = methodDeclaration.Replace("partial void ", "private void base_");

                if (methodDeclaration.EndsWith("OnCreated()"))
                {
                    methodDeclaration = methodDeclaration.Replace("partial void On", "protected override void BeginOn");
                }

                bool skip = false;
                foreach (string foreignKey in table.ForeignKeys)
                {
                    if (methodName == "On" + foreignKey + "Changing")
                    {
                        skip = true;
                        break;
                    }
                }

                if (skip)
                    continue;

                output.Add("\t\t" + methodDeclaration);
                output.Add("\t\t{");
                if (methodName.EndsWith("Changing"))
                {
                    string propertyName = methodName.Substring(2, methodName.Length - 10);
                    output.Add("\t\t\tm_" + propertyName + ".CheckEdittable();");
                }
                if (methodName.EndsWith("OnCreated"))
                {
                    foreach (PropertyInfo sets in table.Properties)
                    {
                        output.Add(string.Format("\t\t\tm_{0} = new Property<{1}>(this, \"{0}\", {2});", sets.PropertyName, sets.Type, sets.IsEntityRef ? "true" : "false"));
                    }

                    bool anyReadOnly = false;
                    foreach (PropertyInfo sets in table.Properties)
                    {
                        if (sets.IsReadOnly)
                        {
                            if (!anyReadOnly) output.Add("");

                            output.Add(string.Format("\t\t\tm_{0}.IsReadOnly = true;", sets.PropertyName));
                            anyReadOnly = true;
                        }
                    }

                    bool anyMaxLength = false;
                    foreach (PropertyInfo sets in table.Properties)
                    {
                        if (sets.MaxLength != null)
                        {
                            if (!anyMaxLength) output.Add("");

                            output.Add(string.Format("\t\t\tm_{0}.MaxLength = {1};", sets.PropertyName, sets.MaxLength));
                            anyMaxLength = true;
                        }
                    }

                    bool anyRequired = false;
                    foreach (PropertyInfo sets in table.Properties)
                    {
                        if (sets.IsRequired && !sets.IsReadOnly)
                        {
                            if (sets.Type == "int" || sets.Type == "bool" || sets.Type == "DateTime" ||
                                sets.Type == "double")
                            {
                                continue;
                            }

                            if (!anyRequired) output.Add("");

                            output.Add(string.Format("\t\t\tAddRequiredField(m_{0}, \"{1} is required.\");", sets.PropertyName, GetPrettyName(sets.PropertyName)));
                            anyRequired = true;
                        }
                    }

                }
                output.Add("\t\t}");
                output.Add("");
            }
            output.Add("\t\tpartial void OnValidate(ChangeAction action)");
            output.Add("\t\t{");
            output.Add("\t\t\tBeginOnValidate(action);");
            output.Add("\t\t}");
            output.Add("\t}");

            if (!string.IsNullOrEmpty(myNamespace))
            {
                output.Add("}");
            }

            string myOutputDirectory = Path.GetDirectoryName(fileName);
            if (!Directory.Exists(myOutputDirectory))
                Directory.CreateDirectory(myOutputDirectory);

            // .Generated file
            File.WriteAllLines(fileName, output.ToArray());
            CleanUpCSharp(fileName);

            return fileName;
        }

        private static string GetPrettyName(string input)
        {
            if (string.IsNullOrEmpty(input) || !char.IsUpper(input[0]))
                return input;

            // Split words based on casing
            List<string> words = new List<string>();
            int oldIndex = 0;
            for (int i = 1; i < input.Length; i++)
            {
                char c = input[i];

                if (char.IsUpper(c))
                {
                    string newWord = input.Substring(oldIndex, i - oldIndex);
                    words.Add(newWord);
                    oldIndex = i;
                }
            }

            words.Add(input.Substring(oldIndex));

            if (words.Count == 0)
                return input;

            // Aliases
            for (int i = 0; i < words.Count; i++)
            {
                switch (words[i])
                {
                    case "Qty":
                        words[i] = "Quantity";
                        break;
                    case "Desc":
                        words[i] = "Description";
                        break;
                    case "Cust":
                        words[i] = "Customer";
                        break;
                }
            }

            // Put words back together
            input = string.Empty;
            for (int i = 0; i < words.Count; i++)
            {
                string word = words[i];

                if (i == 0 && words.Count > 1 && word.EndsWith("_"))
                    continue;

                if (i > 0 && words[i - 1].Length == 1 && word.Length == 1)
                {
                    input += word;
                }
                else
                {
                    if (string.IsNullOrEmpty(input))
                        input = word;
                    else
                        input += " " + word;
                }
            }

            return input;
        }

        private static void CleanUpCSharp(string projectFileOutputPath)
        {
            Guard.ArgumentNotNullOrEmptyString(projectFileOutputPath, "projectFileOutputPath");

            string[] lines = File.ReadAllLines(projectFileOutputPath);
            StringBuilder newLines = new StringBuilder();

            bool lastLineIsBlank = false;
            bool lastLineIsOpenBrace = false;

            foreach (string line in lines)
            {
                bool lineIsBlank = false;
                bool lineIsCloseBrace = false;
                bool lineIsOpenBrace = false;

                string modLine = line.Replace("\t", "").Replace(" ", "");
                if (string.IsNullOrEmpty(modLine))
                {
                    lineIsBlank = true;
                }
                else
                {
                    if (modLine == "{")
                        lineIsOpenBrace = true;
                    else if (modLine == "}")
                        lineIsCloseBrace = true;
                }

                if (lineIsBlank && lastLineIsBlank)
                {
                    // don't output line
                }
                else if (lineIsCloseBrace && lastLineIsBlank)
                {
                    // remove last line
                    newLines.Remove(newLines.Length - 2, 2);
                    newLines.AppendLine(line.Replace("\t", "    ").TrimEnd(' '));
                }
                else if (lineIsBlank && lastLineIsOpenBrace)
                {
                    // don't output line

                    // set to previous line state
                    lineIsOpenBrace = true;
                    lineIsBlank = false;
                }
                else
                {
                    newLines.AppendLine(line.Replace("\t", "    ").TrimEnd(' '));
                }

                lastLineIsBlank = lineIsBlank;
                lastLineIsOpenBrace = lineIsOpenBrace;
            }

            File.WriteAllText(projectFileOutputPath, newLines.ToString());
        }

        internal static string ConvertTypeToNullable(string value)
        {
            if (value == null)
                return null;

            const string systemNullable = "System.Nullable<";
            if (value.StartsWith(systemNullable))
            {
                value = value.Substring(systemNullable.Length, value.Length - systemNullable.Length - 1) + "?";
            }

            if (!value.Contains("_") && !value.Contains("Linq") && !value.Contains("?"))
            {
                switch (value)
                {
                    case "int":
                        value = "int?";
                        break;

                    case "string":
                        break;

                    case "System.Guid":
                        value = "Guid?";
                        break;

                    case "System.DateTime":
                        value = "DateTime?";
                        break;

                    case "byte":
                        value = "byte?";
                        break;

                    case "short":
                        value = "short?";
                        break;

                    case "decimal":
                        value = "decimal?";
                        break;

                    case "bool":
                        value = "bool?";
                        break;

                    default:
                        break;
                }
            }

            if (value.StartsWith("System."))
            {
                value = value.Substring("System.".Length);
                if (value.Contains("."))
                    value = "System." + value;
            }

            return value;
        }
    }
}
